Micron Document
🎖️GitЯра🎖️

specs/20260507-161858-app-docs-markdown/data-model.md docs/obtainium-generated-deeplinks (42f311dd) Text, 10.74 KB

Tc9d1d9# Data Model: App Documentation (Android/KMP)

Tc9d1d9## Overview

This feature introduces no Room entities and no durable database tables. Documentation content is packaged as build-time resources/assets and loaded into memory at runtime. Preferences are optional and limited to lightweight UX state (for example, remembering the last viewed section); the documentation corpus itself is never stored in Room.

The core runtime model is expressed as Kotlin Ta5d6ff`data class` and Ta5d6ff`sealed interface` types that can live in Ta5d6ff`feature/docs/src/commonMain/kotlin/...` and be shared across Android, Desktop, and iOS.

---

Tc9d1d9## Runtime Entities

Tc9d1d9### 1. `DocSection`

Represents the top-level documentation buckets shown on the website and inside the app.

Ta5d6ff```Ta5d6ffkotlin
Tf0883e@Serializable
Tff7b72sealed Tff7b72interface T56d364DocSection Tb4b4b4{
Tf0883e@Serializable Tff7b72data Tff7b72object T56d364UserGuide Tb4b4b4: Te6edf3DocSection
Tf0883e@Serializable Tff7b72data Tff7b72object T56d364DeveloperGuide Tb4b4b4: Te6edf3DocSection
Tb4b4b4}
Ta5d6ff```

| Property | Type | Notes |
|----------|------|-------|
| Ta5d6ff`id` | derived | Stable logical ID such as Ta5d6ff`user` or Ta5d6ff`developer` |
| Ta5d6ff`displayName` | derived | UI label shown in TOC/search grouping |
| Ta5d6ff`resourceDir` | derived | Ta5d6ff`docs/user/` or Ta5d6ff`docs/developer/` |

**Validation rules**
Tff7b72- Must map 1:1 to a top-level docs directory.
Tff7b72- Must be stable across releases so deep links and keyword index entries remain valid.

---

Tc9d1d9### 2. `DocPage`

Represents a single documentation page regardless of how it is rendered on a target.

Ta5d6ff```Ta5d6ffkotlin
Tf0883e@Serializable
Tff7b72data Tff7b72class T56d364DocPageTb4b4b4(
Tff7b72val Te6edf3idTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3titleTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3sectionTb4b4b4: Te6edf3DocSectionTb4b4b4,
Tff7b72val Te6edf3navOrderTb4b4b4: Tffa657IntTb4b4b4,
Tff7b72val Te6edf3resourcePathTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3keywordsTb4b4b4: Te6edf3ListTff7b72<Tffa657StringTff7b72>Tb4b4b4,
Tff7b72val Te6edf3aliasesTb4b4b4: Te6edf3ListTff7b72<Tffa657StringTff7b72> Tff7b72= Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4,
Tff7b72val Te6edf3charCountTb4b4b4: Tffa657IntTb4b4b4,
Tb4b4b4)
Ta5d6ff```

| Field | Type | Description |
|-------|------|-------------|
| Ta5d6ff`id` | Ta5d6ff`String` | Stable slug such as Ta5d6ff`messages-and-channels` |
| Ta5d6ff`title` | Ta5d6ff`String` | Human-readable page title |
| Ta5d6ff`section` | Ta5d6ff`DocSection` | User or Developer Guide |
| Ta5d6ff`navOrder` | Ta5d6ff`Int` | Intended sort order within the section |
| Ta5d6ff`resourcePath` | Ta5d6ff`String` | Canonical packaged resource path, for example Ta5d6ff`docs/user/messages-and-channels.html` |
| Ta5d6ff`keywords` | Ta5d6ff`List<String>` | Search/retrieval vocabulary generated at build time |
| Ta5d6ff`aliases` | Ta5d6ff`List<String>` | Optional alternative search terms or renamed page slugs |
| Ta5d6ff`charCount` | Ta5d6ff`Int` | Plain-text character count used for token budgeting |

**Validation rules**
Tff7b72- Ta5d6ff`id` must be unique across the full corpus.
Tff7b72- Ta5d6ff`navOrder` must be non-negative.
Tff7b72- Ta5d6ff`resourcePath` must resolve in the packaged bundle for every supported target.
Tff7b72- Ta5d6ff`charCount` must be Ta5d6ff`> 0`.

**State transitions**
Tff7b72- Immutable after load.
Tff7b72- Replaced only when the shipped app version changes.

---

Tc9d1d9### 3. `DocPageContent`

Decouples metadata from actual content so different targets can choose HTML or markdown rendering.

Ta5d6ff```Ta5d6ffkotlin
Tff7b72data Tff7b72class T56d364DocPageContentTb4b4b4(
Tff7b72val Te6edf3pageTb4b4b4: Te6edf3DocPageTb4b4b4,
Tff7b72val Te6edf3htmlTb4b4b4: Tffa657String? Tff7b72= Tff7b72nullTb4b4b4,
Tff7b72val Te6edf3markdownTb4b4b4: Tffa657String? Tff7b72= Tff7b72nullTb4b4b4,
Tff7b72val Te6edf3cssPathTb4b4b4: Tffa657String? Tff7b72= Tff7b72nullTb4b4b4,
Tb4b4b4)
Ta5d6ff```

| Field | Type | Notes |
|-------|------|-------|
| Ta5d6ff`page` | Ta5d6ff`DocPage` | Metadata and lookup info |
| Ta5d6ff`html` | Ta5d6ff`String?` | Preferred on Android/WebView and for site output parity |
| Ta5d6ff`markdown` | Ta5d6ff`String?` | Optional fallback for Compose markdown rendering on Desktop/iOS |
| Ta5d6ff`cssPath` | Ta5d6ff`String?` | Shared stylesheet path for HTML surfaces |

**Rendering rules**
Tff7b72- Android normally prefers Ta5d6ff`html`.
Tff7b72- Desktop/iOS may prefer Ta5d6ff`markdown` for Compose rendering, or Ta5d6ff`html` if an embedded browser implementation is chosen.
Tff7b72- At least one of Ta5d6ff`html` or Ta5d6ff`markdown` must be present for each page.

---

Tc9d1d9### 4. `DocBundle`

Runtime aggregate of the full packaged documentation corpus.

Ta5d6ff```Ta5d6ffkotlin
Tff7b72data Tff7b72class T56d364DocBundleTb4b4b4(
Tff7b72val Te6edf3pagesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DocPageTff7b72>Tb4b4b4,
Tff7b72val Te6edf3pageIndexTb4b4b4: Te6edf3MapTff7b72<Tffa657StringTb4b4b4, Te6edf3DocPageTff7b72>Tb4b4b4,
Tff7b72val Te6edf3bundleVersionTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3generatedAtTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3totalBytesTb4b4b4: Tffa657LongTb4b4b4,
Tb4b4b4)
Ta5d6ff```

| Field | Type | Description |
|-------|------|-------------|
| Ta5d6ff`pages` | Ta5d6ff`List<DocPage>` | All bundled pages |
| Ta5d6ff`pageIndex` | Ta5d6ff`Map<String, DocPage>` | O(1) lookup by page ID |
| Ta5d6ff`bundleVersion` | Ta5d6ff`String` | App/docs version identifier (Ta5d6ff`beta`, Ta5d6ff`2.8.0`, etc.) |
| Ta5d6ff`generatedAt` | Ta5d6ff`String` | ISO timestamp written by the build task |
| Ta5d6ff`totalBytes` | Ta5d6ff`Long` | Total packaged size for size-budget enforcement |

**Primary operations**

Ta5d6ff```Ta5d6ffkotlin
Tff7b72interface T56d364DocBundleLoader Tb4b4b4{
Tff7b72suspend Tff7b72fun Td2a8ffloadTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3DocBundle
Tff7b72suspend Tff7b72fun Td2a8ffreadPageTb4b4b4(Te6edf3pageIdTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3DocPageContent?
Tff7b72fun Td2a8ffpagesBySectionTb4b4b4(Te6edf3sectionTb4b4b4: Te6edf3DocSectionTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3DocPageTff7b72>
Tb4b4b4}
Ta5d6ff```

**Invariants**
Tff7b72- Ta5d6ff`pagesBySection()` sorts by Ta5d6ff`navOrder`, then title.
Tff7b72- Ta5d6ff`pageIndex.keys == pages.map { it.id }.toSet()`.
Tff7b72- Ta5d6ff`totalBytes <= 10_485_760` for release-ready bundles.

---

Tc9d1d9### 5. `KeywordIndexEntry`

Build-time artifact decoded at runtime for keyword search and AI retrieval.

Ta5d6ff```Ta5d6ffkotlin
Tf0883e@Serializable
Tff7b72data Tff7b72class T56d364KeywordIndexEntryTb4b4b4(
Tff7b72val Te6edf3idTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3titleTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3sectionTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3resourcePathTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3navOrderTb4b4b4: Tffa657IntTb4b4b4,
Tff7b72val Te6edf3keywordsTb4b4b4: Te6edf3ListTff7b72<Tffa657StringTff7b72>Tb4b4b4,
Tff7b72val Te6edf3aliasesTb4b4b4: Te6edf3ListTff7b72<Tffa657StringTff7b72> Tff7b72= Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4,
Tff7b72val Te6edf3charCountTb4b4b4: Tffa657IntTb4b4b4,
Tb4b4b4)
Ta5d6ff```

| Field | Type | Description |
|-------|------|-------------|
| Ta5d6ff`id` | Ta5d6ff`String` | Matches Ta5d6ff`DocPage.id` |
| Ta5d6ff`title` | Ta5d6ff`String` | Display title |
| Ta5d6ff`section` | Ta5d6ff`String` | Ta5d6ff`user` or Ta5d6ff`developer` |
| Ta5d6ff`resourcePath` | Ta5d6ff`String` | Packaged path to HTML/markdown asset |
| Ta5d6ff`navOrder` | Ta5d6ff`Int` | Frontmatter-derived ordering |
| Ta5d6ff`keywords` | Ta5d6ff`List<String>` | Generated retrieval terms |
| Ta5d6ff`aliases` | Ta5d6ff`List<String>` | Optional renamed terms and synonyms |
| Ta5d6ff`charCount` | Ta5d6ff`Int` | Plain-text size used for token budgeting |

**Validation rules**
Tff7b72- Must match the JSON schema in Ta5d6ff`contracts/keyword-index-schema.json`.
Tff7b72- Every entry must correspond to exactly one bundled page.

---

Tc9d1d9### 6. `DocSearchQuery` and `DocSearchResult`

Used by the shared keyword-search fallback and by Gemini Nano retrieval pre-ranking.

Ta5d6ff```Ta5d6ffkotlin
Tff7b72data Tff7b72class T56d364DocSearchQueryTb4b4b4(
Tff7b72val Te6edf3rawTextTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3normalizedTermsTb4b4b4: Te6edf3ListTff7b72<Tffa657StringTff7b72>Tb4b4b4,
Tb4b4b4)

Tff7b72data Tff7b72class T56d364DocSearchResultTb4b4b4(
Tff7b72val Te6edf3pageTb4b4b4: Te6edf3DocPageTb4b4b4,
Tff7b72val Te6edf3scoreTb4b4b4: Tffa657IntTb4b4b4,
Tff7b72val Te6edf3matchedTermsTb4b4b4: Te6edf3ListTff7b72<Tffa657StringTff7b72>Tb4b4b4,
Tb4b4b4)
Ta5d6ff```

| Type | Purpose |
|------|---------|
| Ta5d6ff`DocSearchQuery` | Normalized user input after lowercasing, tokenization, alias expansion, and stop-word removal |
| Ta5d6ff`DocSearchResult` | Ranked page match used in UI search results and AI context selection |

**Ranking rules**
Tff7b72- Exact keyword matches score higher than alias matches.
Tff7b72- Title matches outrank body-keyword matches.
Tff7b72- Ta5d6ff`navOrder` breaks ties within a section.

---

Tc9d1d9### 7. `AIDocAssistant`

Shared abstraction over the platform-specific docs assistant.

Ta5d6ff```Ta5d6ffkotlin
Tff7b72interface T56d364AIDocAssistant Tb4b4b4{
T8b949e/** Answer a user question about Meshtastic using bundled documentation context. */
Tff7b72suspend Tff7b72fun Td2a8ffanswerTb4b4b4(Te6edf3questionTb4b4b4: Tffa657StringTb4b4b4, Te6edf3currentPageIdTb4b4b4: Tffa657String? Tff7b72= Tff7b72nullTb4b4b4)Tb4b4b4: Te6edf3AIDocAssistantResult

T8b949e/** Answer a user question about Meshtastic, streaming the results as they arrive. */
Tff7b72fun Td2a8ffanswerStreamTb4b4b4(
Te6edf3questionTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3currentPageIdTb4b4b4: Tffa657String? Tff7b72= Tff7b72nullTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3kotlinxTb4b4b4.Te6edf3coroutinesTb4b4b4.Te6edf3flowTb4b4b4.Te6edf3FlowTff7b72<Te6edf3AIDocAssistantResultTff7b72>
Tb4b4b4}
Ta5d6ff```

Possible runtime result model:

Ta5d6ff```Ta5d6ffkotlin
Tff7b72sealed Tff7b72interface T56d364AIDocAssistantResult Tb4b4b4{
Tff7b72data Tff7b72class T56d364PartialTb4b4b4(
Tff7b72val Te6edf3answerTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3sourcePagesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DocPageTff7b72>Tb4b4b4,
Tff7b72val Te6edf3usedOnDeviceModelTb4b4b4: Tffa657BooleanTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3AIDocAssistantResult

Tff7b72data Tff7b72class T56d364SuccessTb4b4b4(
Tff7b72val Te6edf3answerTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3sourcePagesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DocPageTff7b72>Tb4b4b4,
Tff7b72val Te6edf3usedOnDeviceModelTb4b4b4: Tffa657BooleanTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3AIDocAssistantResult

Tff7b72data Tff7b72class T56d364FallbackTb4b4b4(
Tff7b72val Te6edf3messageTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3suggestedPagesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DocPageTff7b72>Tb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3AIDocAssistantResult

Tff7b72data Tff7b72class T56d364ErrorTb4b4b4(
Tff7b72val Te6edf3reasonTb4b4b4: Te6edf3DocsAiErrorTb4b4b4,
Tff7b72val Te6edf3suggestedPagesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DocPageTff7b72> Tff7b72= Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3AIDocAssistantResult
Tb4b4b4}
Ta5d6ff```

Associated error model:

Ta5d6ff```Ta5d6ffkotlin
Tff7b72sealed Tff7b72interface T56d364DocsAiError Tb4b4b4{
Tff7b72data Tff7b72object T56d364UnsupportedPlatform Tb4b4b4: Te6edf3DocsAiError
Tff7b72data Tff7b72object T56d364UnsupportedFlavor Tb4b4b4: Te6edf3DocsAiError
Tff7b72data Tff7b72object T56d364ModelUnavailable Tb4b4b4: Te6edf3DocsAiError
Tff7b72data Tff7b72object T56d364Busy Tb4b4b4: Te6edf3DocsAiError
Tff7b72data Tff7b72object T56d364TokenBudgetExceeded Tb4b4b4: Te6edf3DocsAiError
Tff7b72data Tff7b72object T56d364Unknown Tb4b4b4: Te6edf3DocsAiError
Tb4b4b4}
Ta5d6ff```

**Platform behavior**
Tff7b72- Android Ta5d6ff`google` flavor may return Ta5d6ff`Success` using Gemini Nano.
Tff7b72- Ta5d6ff`fdroid`, Desktop, and iOS normally return Ta5d6ff`Fallback` or Ta5d6ff`UnsupportedPlatform` and provide suggested pages.

---

Tc9d1d9### 8. `AIDocAssistantSessionState`

UI state for the Chirpy conversation surface.

Ta5d6ff```Ta5d6ffkotlin
Tff7b72data Tff7b72class T56d364AIDocAssistantSessionStateTb4b4b4(
Tff7b72val Te6edf3messagesTb4b4b4: Te6edf3ListTff7b72<Te6edf3ChirpyMessageTff7b72>Tb4b4b4,
Tff7b72val Te6edf3isLoadingTb4b4b4: Tffa657BooleanTb4b4b4,
Tff7b72val Te6edf3draftQuestionTb4b4b4: Tffa657StringTb4b4b4,
Tb4b4b4)

Tf0883e@Serializable
Tff7b72data Tff7b72class T56d364SourceRefTb4b4b4(
Tff7b72val Te6edf3idTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3titleTb4b4b4: Tffa657StringTb4b4b4,
Tb4b4b4)

Tf0883e@Serializable
Tff7b72data Tff7b72class T56d364ChirpyMessageTb4b4b4(
Tff7b72val Te6edf3idTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3roleTb4b4b4: Te6edf3ChirpyRoleTb4b4b4,
Tff7b72val Te6edf3textTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72val Te6edf3sourcesTb4b4b4: Te6edf3ListTff7b72<Te6edf3SourceRefTff7b72> Tff7b72= Te6edf3emptyListTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Tf0883e@Serializable
Tff7b72enum Tff7b72class T56d364ChirpyRole Tb4b4b4{ Te6edf3USERTb4b4b4, Te6edf3ASSISTANTTb4b4b4, Te6edf3SYSTEM Tb4b4b4}
Ta5d6ff```

**Lifecycle**
Tff7b72- Session state is ephemeral and resets when the screen/process is recreated unless explicitly made saveable.
Tff7b72- Messages are not persisted to Room.

---

Tc9d1d9## Build-Time Artifacts

| Artifact | Produced By | Consumed By | Notes |
|----------|-------------|-------------|-------|
| Ta5d6ff`docs/**/*.md` | Human-authored | Jekyll + Gradle docs task | Canonical source |
| Generated HTML pages | Gradle docs generation task | Android Ta5d6ff`WebView`, optional Desktop/iOS embedded browser, GitHub Pages output | Site-parity artifact |
| Optional bundled markdown mirror | Gradle docs generation task | Desktop/iOS Compose renderer | Keeps shared renderer path available |
| Ta5d6ff`index.json` | Gradle docs generation task | Search, AI retrieval, bundle loader | Must match schema contract |
| Ta5d6ff`versions.yml` | Release workflow | Jekyll version selector | Web-only manifest |
| Screenshot PNGs | Roborazzi/Paparazzi/manual capture sync | Markdown pages, packaged docs assets | Inline illustrations |
| Ta5d6ff`docs.css` | Hand-authored/shared | HTML pages | Light/dark + callouts |
| Chirpy SVG/vector | Design assets | Compose UI | Branded assistant avatar |

---

Tc9d1d9## Relationships

Ta5d6ff```Ta5d6fftext
DocBundle
├── pages: List<DocPage>
├── pageIndex: Map<String, DocPage>
└── page content files (HTML and/or markdown)

KeywordIndexEntry --1:1--> DocPage
DocSearchQuery --ranks--> DocSearchResult --references--> DocPage
AIDocAssistant --uses--> KeywordIndexEntry + DocPageContent
AIDocAssistantSessionState --contains--> ChirpyMessage --references--> DocPage IDs
Ta5d6ff```

---

Tc9d1d9## Persistence Notes

Tff7b72- **No Room tables** are required for documentation content.
Tff7b72- **No migration story** is required for docs content because the corpus is versioned with the app binary.
Tff7b72- Optional UX-only settings (for example, last-opened section) may live in Ta5d6ff`core:prefs`, but they are intentionally excluded from this feature’s core data model.

Served by rngit 1.5.2 - Generated in 0.06s